Skip to content

Support Joy-Con 2 clones that only speak the console protocol (NYXI Hyperion 3) - #26

Open
mygamecache wants to merge 1 commit into
JoeGeC:mainfrom
mygamecache:nyxi-controller-support
Open

mygamecache wants to merge 1 commit into
JoeGeC:mainfrom
mygamecache:nyxi-controller-support

Conversation

@mygamecache

@mygamecache mygamecache commented Sep 23, 2026 •

Copy link
Copy Markdown

Support Joy-Con 2 clones that only speak the console protocol (NYXI Hyperion 3)

Follow-up to our Reddit chat. This adds support for third-party Joy-Con 2 clones that currently can't connect: pairing fails with an "incorrect PIN or passkey" error and the controller disconnects. Tested with a NYXI Hyperion 3 (left and right).

Why they fail today

  • They copy the Joy-Con 2 GATT table but ignore the write characteristic (649d4ac9…) and never notify on …FD2. They only answer on the side-specific channel a Switch 2 console uses (commands on 65a724b3… / ce49a830… behind 17 zero bytes, input on d5a9e01e… / cc1bbbb5…).
  • They send an SMP Security Request on every connection. Android shows a pairing dialog, Just Works always fails, and about 3 s after the pairing gives up the stack drops the link (l2cu_start_post_bond_timer), so sessions end at ~33 s.

What this changes

  • Detection (JoyconConnection): a controller that sends an SMP Security Request, or that neither replies to the SPI read nor streams on …FD2 within 1.5 s of init, is handed to ConsoleSession. The second check is needed because Android runs one pairing at a time, so a second clone connected while the first one's pairing is pending sends no request. Genuine Joy-Con 2 connections keep the existing flow.
  • connection/console/: the console start-up sequence including Nintendo pairing (report 0x15), the console input report translated into the existing button bitmask (so mapping, the gamepad and DSU are unchanged), player LEDs, and battery level.
  • Keeping the link up: SecurityRequestReceiver aborts the ordered pairing broadcast so no dialog appears, and LinkHolder keeps a pending LE L2CAP channel on the link so the post-pairing disconnect never fires.
  • Latency: these follow the app's high-priority toggle, but for them "high" asks the hidden requestLeConnectionUpdate (via HiddenApiBypass) for 7.5 ms, the LE minimum, falling back to CONNECTION_PRIORITY_HIGH. That's ~200 reports/s instead of ~67. Each console session asks again when another controller joins, since Android can slow an existing one down when one does.
  • Host address: pairing needs the phone's Bluetooth address, read with settings get secure bluetooth_address through the existing Shizuku access. If that isn't available, pairing is skipped.
  • Docs: a docs/protocol.md section (characteristics, pairing, report layout, the Android workarounds) and one README troubleshooting line. Unit tests cover the report parser and the pairing maths, using captured controller replies.

Overlap with #19

@SWeav02's draft in #19 covers the same controller from a different angle, and I only spotted it after building this. Take whichever suits the project, or pieces of both — I'm happy either way, and glad to help test theirs. The main differences:

  • This one runs the console start-up sequence and Nintendo pairing rather than parsing the packets the controller sends unprompted, so the stored host address is in place for button-press reconnect later.
  • This one suppresses the pairing dialog and holds the link open, rather than avoiding the second advertisement.
  • This one asks for a 7.5 ms interval (~200 reports/s) instead of staying on CONNECTION_PRIORITY_HIGH (~67).
  • Add support for Nyxi Hyperion 3 and dual-controller latency fix #19 also fixes the Konsist tests on Windows and sets high priority for genuine controllers; neither is in here.

Their observation that Android slows an existing connection when a second controller joins is real, and it applies here too, so each console session re-asserts its interval when another joins. Credit to them for spotting it.

Tested

  • RedMagic Astra, Android 16: both NYXI Joy-Cons at once, connected back to back and 30+ s apart, paired onto one player with the L+R combo, sideways layout, no pairing dialog, no drops over several minutes. In an earlier standalone test app using the same approach, both controllers ran at 7.5 ms with 0 lost reports and a worst gap of 23–30 ms.
  • Rebased on 1.3.0; the console path uses the new high-priority toggle rather than its own switch.
  • ./gradlew build passes (compile, unit tests, lint). :konsist:test passes too, though on Windows it needs the path fix from Add support for Nyxi Hyperion 3 and dual-controller latency fix #19 to run at all — unrelated to this change.

Please check

  • Genuine Joy-Con 2: I don't own one. The only new behaviour on that path is the 1.5 s silence check, which should never trigger because genuine controllers answer the SPI read right away. It's worth confirming they still connect normally.
  • AOSP internals: the dialog suppression, link hold and 7.5 ms interval rely on Android Bluetooth stack behaviour and a hidden API, so a future Android release could break them. Happy to put the fast interval behind a setting, or drop it, if you'd prefer.

Not included

  • Motion: the console report's motion block is in an undocumented packed format, so it reads as zero.
  • Reconnecting with a button press after a drop or sleep; SYNC is still needed, as with genuine controllers.

One unrelated note worth a troubleshooting line (added to the README): on some OEM builds the game launcher clears background apps when a game starts and kills the foreground service, which drops every controller mid-game. Setting the app's battery usage to unrestricted and excluding it from that cleanup fixes it. Took me a while to realise it wasn't the app's fault.

Happy to test changes on the NYXI controllers and send logs.

NYXI Hyperion 3 Joy-Cons copy the Joy-Con 2 GATT table but ignore the
common command and input characteristics, answering only on the
side-specific channel a Switch 2 console uses. They are recognised by the
SMP security request they send on connect, or by silence on the common
channel after init, and are then driven by the console start-up sequence
with Nintendo pairing and the console input report.

Android's pairing with these controllers always fails, so the pairing
dialog is suppressed and a pending L2CAP channel keeps the link from
being dropped afterwards. A 7.5 ms connection interval is requested, with
CONNECTION_PRIORITY_HIGH as the fallback, and every console session asks
for its interval again when another controller joins the pool.

Genuine Joy-Con 2 connections are unchanged.
@mygamecache
mygamecache force-pushed the nyxi-controller-support branch from 3ee6292 to fe81675 Compare September 23, 2026 23:48
@mygamecache

Copy link
Copy Markdown
Author

One design question, since 1.3.0 landed while I was writing this.

The fast interval now rides on the high-priority toggle, which DsuMotionPolicy drives from the DSU server. That fits motion, but for these clones the interval matters for plain gamepad use too: at balanced they sit near 30 ms, while 7.5 ms gives ~200 reports/s. As it stands, someone using one as a normal controller, with DSU off, gets the slow path.

Options as I see them:

  1. Leave it as is — one toggle, motion-driven.
  2. Always request the fastest interval for console-protocol controllers, since the trade-off the toggle exists for (battery) applies the same way, and these are the controllers where the difference is largest.
  3. A separate setting for the connection interval, independent of DSU.

Happy to implement whichever you prefer — I have the hardware to measure any of them.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant